package com.virjar.xposedhooktool.tool.okhttp;

import android.support.annotation.NonNull;

import com.google.common.collect.Maps;
import com.virjar.xposedhooktool.hotload.SingletonXC_MethodHook;
import com.virjar.xposedhooktool.tool.ReflectUtil;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadPoolExecutor;

import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;

/**
 * Created by virjar on 2018/3/27.<br>
 * 异步任务堆栈监听器
 */

public class ThreadPoolHook {
    //注意，千万不要使用InheritableThreadLocal
    private static ThreadLocal<Throwable> stackTraceThreadLocal = new ThreadLocal<>();

    private static Throwable getThreadSubmitEntry() {
        return stackTraceThreadLocal.get();
    }

    public static Throwable stackTraceChain() {
        throw new UnsupportedOperationException("please use com.virjar.xposedhooktool.tool.okhttp.ThreadPoolHookV2.stackTraceChain");
//        Throwable submitEntry = getThreadSubmitEntry();
//        if (submitEntry == null) {
//            return new Throwable();
//        }
//        return new Throwable(submitEntry);
    }

    @Deprecated
    public static void monitorThreadPool() {
        XposedHelpers.findAndHookMethod(ThreadPoolExecutor.class, "execute", Runnable.class, new SingletonXC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                Runnable target = (Runnable) param.args[0];
                Throwable parentStackTrace = getThreadSubmitEntry();
                Throwable theStackTrace;
                if (parentStackTrace != null) {
                    theStackTrace = new Throwable("parent submit task stack entry", parentStackTrace);
                } else {
                    theStackTrace = new Throwable("parent submit task stack entry");
                }
                if (target instanceof Comparable) {
                    param.args[0] = new ComparableRunnableMonitor(target, theStackTrace);
                } else {
                    param.args[0] = new RunnableMonitor(target, theStackTrace);
                }
            }
        });


        Method threadInitMethod = null;
        try {
            threadInitMethod = Thread.class.getDeclaredMethod("init", ThreadGroup.class, Runnable.class, String.class, long.class);
            //threadInitMethod = XposedHelpers.findMethodExactIfExists(Thread.class, "init", ThreadGroup.class, Runnable.class, String.class, long.class);
        } catch (Exception e) {
            //ignore
        }
        if (threadInitMethod == null) {
            try {
                threadInitMethod = Thread.class.getDeclaredMethod("create", ThreadGroup.class, Runnable.class, String.class, long.class);
                //threadInitMethod = XposedHelpers.findMethodExactIfExists(Thread.class, "init", ThreadGroup.class, Runnable.class, String.class, long.class);
            } catch (Exception e) {
                //ignore
            }
        }
        if (threadInitMethod != null) {
            XposedBridge.hookMethod(threadInitMethod, new SingletonXC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Runnable target = (Runnable) param.args[1];
                    //传递了runnable的方式创建线程
                    Throwable parentStackTrace = getThreadSubmitEntry();
                    Throwable theStackTrace;
                    if (parentStackTrace != null) {
                        theStackTrace = new Throwable("parent submit task stack entry", parentStackTrace);
                    } else {
                        theStackTrace = new Throwable("parent submit task stack entry");
                    }
                    if (target != null) {
                        param.args[1] = new RunnableMonitor(target, theStackTrace);
                        return;
                    }

                    // run 方法在thread本身实现
                    Thread thread = (Thread) param.thisObject;
                    stackTraceMap.putIfAbsent(thread, theStackTrace);
                    ReflectUtil.findAndHookMethodWithSupperClass(thread.getClass(), "run", new SingletonXC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            if (stackTraceThreadLocal.get() == null) {
                                Throwable throwable = stackTraceMap.get(Thread.currentThread());
                                if (throwable != null) {
                                    stackTraceThreadLocal.set(throwable);
                                }
                            }
                        }

                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            stackTraceThreadLocal.remove();
                            stackTraceMap.remove(Thread.currentThread());
                        }
                    });
                }
            });
        }
    }

    private static ConcurrentMap<Thread, Throwable> stackTraceMap = Maps.newConcurrentMap();

    private static class ComparableRunnableMonitor extends RunnableMonitor implements Comparable<ComparableRunnableMonitor> {

        public ComparableRunnableMonitor(Runnable delegate, Throwable parentThreadStackTrace) {
            super(delegate, parentThreadStackTrace);
        }

        @Override
        public int compareTo(@NonNull ComparableRunnableMonitor o) {
            Comparable theComparable = (Comparable) delegate;
            return theComparable.compareTo(o.delegate);
        }
    }


    private static class RunnableMonitor implements Runnable {
        Runnable delegate;
        private Throwable parentThreadStackTrace;

        RunnableMonitor(Runnable delegate, Throwable parentThreadStackTrace) {
            this.delegate = delegate;
            this.parentThreadStackTrace = parentThreadStackTrace;
        }

        @Override
        public void run() {
//            if (stackTraceThreadLocal.get() != null) {
//                delegate.run();
//                return;
//            }

            stackTraceThreadLocal.set(parentThreadStackTrace);
            try {
                delegate.run();
            } finally {
                stackTraceThreadLocal.remove();
            }
        }
    }
}
